package se.grp03.fmtuner.scheduler.io;

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.util.Scanner;

import se.grp03.fmtuner.scheduler.ChannelManager;
import se.grp03.fmtuner.scheduler.message.MessageFactory;
import se.grp03.fmtuner.scheduler.message.MessageInterface;

public class UdpManager {
	private InetAddress 		agentAddress, appAddress;
	private DatagramSocket 		agentSocket, appSocket;
	private Thread 				agentThread, appThread;
	private int					appPort;
	private boolean				debug;
	private Scanner 			debugInput;
	private static UdpManager 	udp; // Singleton
	
	private UdpManager(boolean debug) {
		this.debug = debug;
		debugInput = new Scanner(System.in);
	}
	
	/**
	 * Gives a reference to the single instance of
	 * the UdpManager singleton, without the need
	 * to pass the debug boolean.
	 * 
	 * Intended for getInstance(boolean) to have
	 * been called at least once before.
	 * 
	 * @return A reference to the UdpManager.
	 */
	public static UdpManager getInstance() {
		return getInstance(false);
	}
	
	/**
	 * Gives a reference to the single instance of
	 * the UdpManager singleton.
	 * @return A reference to the UdpManager.
	 */
	public static UdpManager getInstance(boolean debug) {
		/*
		 * If there is no instance of UdpManager, 
		 * then create one and only one instance.
		 */
		if(udp == null)
			udp = new UdpManager(debug);
		return udp;
	}
	
	/**
	 * Opens sockets and connections, and listens
	 * for incoming communications.
	 */
	public void start() {
		agentSocket = null;
		appSocket = null;
		
		// Open server-side UDP connection.
		try {
			appSocket = new DatagramSocket(UdpConstants.APP_PORT);
			System.out.println("** Scheduler Server listening on port "
					+ appSocket.getLocalPort());
		} catch (Exception e) {
			System.err.println("ERROR: Cannot start Scheduler server.");
			System.err.println(e);
			stop();
			System.exit(1);
		}
		
		// Obtain address of FM Tuner Agent for later communications.
		try {
			//System.setProperty("java.net.preferIPv4Stack" , "true");
			agentAddress = InetAddress.getByName(UdpConstants.AGENT_IP);
			System.out.println("** Ready to send UDP messages to " + agentAddress
					+ ":" + UdpConstants.AGENT_PORT);
		} catch (IOException e) {
			System.err.println(e);
			System.err.println("ERROR! Closing all connections...");
			stop();
		}
		
		// Listen for incoming communications from App.
		appThread = new Thread(new Runnable() {
			@Override
			public void run() {
				while(true) {
					// Incoming message from UDP.
					try {
						// Accept incoming message.
						String incomingRequest = null;
						String incomingMessage = null;
						if(debug) {
							// Incoming message from STDIN.
							System.out.print("-> Enter message: ");
				    		incomingRequest = debugInput.nextLine();
						}
						else {
							byte[] incByte = new byte[1024];
							DatagramPacket incPacket = new DatagramPacket(incByte, incByte.length);
							System.out.println("** Awaiting UDP message.");
							appSocket.receive(incPacket);
							appAddress = incPacket.getAddress();
							appPort = incPacket.getPort();
							System.out.println("-> Incoming message from " + appSocket + ":" + appPort);
							incomingMessage = new String(incPacket.getData(), 0, incPacket.getLength(), "UTF-8");
							System.out.println("-> Incoming message: " + incomingMessage);
						}
						
						// Create message from incoming request.
						MessageFactory mf = MessageFactory.getInstance();
						MessageInterface message = mf.createMessage(incomingMessage);
						if(message != null) {
							// Process valid message from App.
							String response = message.process();
							
							if(debug) {
								// Send response message to STDOUT.
								System.out.println("<- Outgoing message: " + response);
							}
							else {
								// Send response message to UDP.
								System.out.println("<- Outgoing message: " + response);
								byte[] msgByte = (response).getBytes();
								DatagramPacket msgPacket = new DatagramPacket(msgByte, msgByte.length,
										appAddress, appPort);
								appSocket.send(msgPacket);
								System.out.println("<- Message sent!");
							}
						}
					} catch (/*IOException e*/ Exception e) {
						System.err.println(e);
					}
				}
			}
		});
        appThread.start();
	}
	
	/**
	 * Stops all communication, closing all sockets.
	 */
	public void stop() {
		// Close server-side sockets and streams.
		try { agentSocket.close(); } catch (Exception e) {}
		try { appSocket.close(); } catch (Exception e) {}
	}
	
	/**
	 * Sends a scan message to the FM Tuner Agent.
	 */
	public void scan() {
		agentThread = new Thread(new Runnable() {
			@Override
			public void run() {
				//isScanning = true;
				try {
					// Send scan message to FM Tuner Agent.
					System.out.println("<- Outgoing message: " + UdpConstants.SCAN);
					agentSocket = new DatagramSocket();
					byte[] msgByte = (UdpConstants.SCAN).getBytes();
					DatagramPacket msgPacket = new DatagramPacket(msgByte, msgByte.length,
							agentAddress, UdpConstants.AGENT_PORT);
					agentSocket.send(msgPacket);
					System.out.println("<- Message sent!");
					
					// Accept incoming messages.
					ChannelManager cm = ChannelManager.getInstance();
					cm.clearChannels();
					byte[] incByte = new byte[1024];
					DatagramPacket incPacket = new DatagramPacket(incByte, incByte.length);
					agentSocket.receive(incPacket);
					System.out.println("-> Incoming channels from " + incPacket.getAddress() + ":" + incPacket.getPort());
					//String incomingMessage = new String(incPacket.getData());
					String incomingMessage = new String(incPacket.getData(), 0, incPacket.getLength(), "UTF-8");
					//incomingMessage.trim();
					while (!incomingMessage.equalsIgnoreCase(UdpConstants.SCAN_END)) {
						try {
							// Add individual channels to list.
							Double channel = Double.parseDouble(incomingMessage); 
							cm.addChannel(channel);
							System.out.println("-> Incoming channel: " + channel);
						} catch (NumberFormatException e) {
							System.err.println("ERROR: Found \"" + incomingMessage 
									+ "\". Expecting channel frequency or \"" 
									+ UdpConstants.SCAN_END + "\".");
						}
						
						// Get next incoming channel
						incByte = new byte[1024];
						incPacket = new DatagramPacket(incByte, incByte.length);
						agentSocket.receive(incPacket);
						incomingMessage = new String(incPacket.getData(), 0, incPacket.getLength(), "UTF-8");
					}
					System.out.println("** Received all incoming channels!");
				} catch (IOException e) {
					System.err.println(e);
				} catch (Exception e) {
					System.err.println(e);
				} finally {
					agentSocket.close();
					//isScanning = false;
				}
			}
		});
        agentThread.start();
	}
	
	/**
	 * Sends a tune message to the FM Tuner Agent.
	 * @param message - The tune message (frequency).
	 */
	public void tune(final String message) {
		agentThread = new Thread(new Runnable() {
			@Override
			public void run() {
				try {
					// Send tune message to FM Tuner Agent.
					System.out.println("<- Outgoing message: " + message);
					agentSocket = new DatagramSocket();
					byte[] msgByte = message.getBytes();
					DatagramPacket msgPacket = new DatagramPacket(msgByte, msgByte.length,
							agentAddress, UdpConstants.AGENT_PORT);
					agentSocket.send(msgPacket);
					System.out.println("<- Message sent!");
					
					// Accept incoming confirmation message.
					byte[] incByte = new byte[1024];
					DatagramPacket incPacket = new DatagramPacket(incByte, incByte.length);
					agentSocket.receive(incPacket);
					System.out.println("-> Incoming message from " + agentSocket.getInetAddress() + ":" + UdpConstants.AGENT_PORT);
					String incomingMessage = new String(incPacket.getData(), 0, incPacket.getLength(), "UTF-8");
					System.out.println("-> Incoming message: " + incomingMessage);
				} catch (IOException e) {
					System.err.println(e);
				} catch (Exception e) {
					System.err.println(e);
				} finally {
					agentSocket.close();
				}
			}
		});
        agentThread.start();
	}
}